package com.laviedesfons.jhmtasc.cadmium;

import com.laviedesfons.jhmtasc.annotations.hooks.MConstructor;
import com.laviedesfons.jhmtasc.annotations.hooks.MMethod;
import com.laviedesfons.jhmtasc.annotations.hooks.MProvider;
import com.laviedesfons.jhmtasc.exceptions.MException;
import com.laviedesfons.jhmtasc.exceptions.MInternalException;
import com.laviedesfons.jhmtasc.exceptions.MRuntimeException;
import com.laviedesfons.jhmtasc.logging.CustomLogger;
import com.laviedesfons.jhmtasc.metadata.TypeUtils;
import com.laviedesfons.jhmtasc.types.abstracts.Interface;
import com.laviedesfons.jhmtasc.types.externals.JAction;
import com.laviedesfons.jhmtasc.types.externals.JArrayList;
import com.laviedesfons.jhmtasc.types.externals.JHashMap;
import com.laviedesfons.jhmtasc.types.interfaces.InternalType;
import com.laviedesfons.jhmtasc.types.interfaces.ToArray;
import fr.x9c.cadmium.kernel.*;
import fr.x9c.cadmium.primitives.cadmium.Cadmium;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author Romano
 */
@PrimitiveProvider
public class MPrimitiveProvider {

    private static final Logger logger = CustomLogger.getLogger();
    private static HashMap<String, Constructor> typeProviders =
            new HashMap<String, Constructor>();

    // tmp fix before Apt usage
    private static final String[] mTagProviders = {
        JArrayList.class.getName(),
        JHashMap.class.getName(),
        JAction.class.getName()
    };

    public MPrimitiveProvider() {
        // tmp fix before Apt usage
        MPrimitiveProvider.addMTagProviders(mTagProviders);
    }

    private static void fail(Exception ex) {
        throw new MRuntimeException(ex);
    }

    public static void addMTagProviders(String[] providers) {
        try {
            for (String className : providers) {
                Class cls = Class.forName(className);

                if (cls.getAnnotation(MProvider.class) != null
                        && !typeProviders.containsKey(cls.getSimpleName())) {

                    for (Constructor c : cls.getConstructors()) {
                        if (c.getAnnotation(MConstructor.class) != null) {
                            typeProviders.put(cls.getSimpleName(), c);
                        }
                    }
                }
            }
        } catch (ClassNotFoundException ex) {
            logger.log(Level.SEVERE,"unknown MTagProvider!", ex);
        }
    }

    private static Interface<InternalType> root(){
        return TypeUtils.getTypeUtils().createRootType();
    }

    @Primitive
    public static Value newObj(
            final CodeRunner ctxt, Value className, Value vargs)
            throws Fail.Exception {

        String cName;
        if ((cName = className.asBlock().asString()) == null) {
            fail(new MException(
                    "unable to get class name from value: not a string"));
            return null;
        }

        // tmp fix before Apt use
        if (typeProviders.isEmpty()) {
            addMTagProviders(mTagProviders);
        }

        Constructor cstr = typeProviders.get(cName);
        if (cstr == null) {
            fail(new MException(
                    "newObj primitive error: no MTagProvider: " + cName));

        }

        try {
            InternalType args = root().fromValue(vargs);
            Object obj = cstr.newInstance(args);
            return Cadmium.createObject(obj);
        } catch (Exception ex) {
            fail(new MException("unable to create instance", ex));
            return null;
        }
    }

    @Primitive
    public static Value callMethod(final CodeRunner ctxt, Value refClass,
            Value methodName, Value args) throws Fail.Exception {

        Object instance;
        String mName;
        if (!(refClass.isBlock()
                && (instance = refClass.asBlock().asCustom()) != null
                && ((mName = methodName.asBlock().asString())) != null)) {
            fail(new MException("call method expects string,int"));
            return null;
        }

        Method target = null;
        for (Method m : instance.getClass().getDeclaredMethods()) {
            if (m.isAnnotationPresent(MMethod.class)) {
                if (mName.equals(m.getName())) {
                    target = m;
                    break;
                }
            }
        }
        if (target == null) {
            fail(new MException(String.format(
                    "%s.%s not available",
                    instance.getClass().getSimpleName(),
                    mName)));
            return null;
        }

        ToArray array;
        try {
            array = (ToArray) root().fromValue(args);
        } catch (MException mex) {
            fail(mex);
            return null;
        }
        Object[] params = array.values().toArray();

        if (target.getParameterTypes().length > 0
                && params !=null
                && target.getParameterTypes().length != params.length) {

            fail(new MException(
                    "Error wrong number of parameters for method: "
                    + mName));
            return null;
        }

        Exception failure;
        try {
            return (Value) target.invoke(instance, (Object[]) params);
         } catch (InvocationTargetException ex){
            if(ex.getCause() instanceof MInternalException){
                throw ((MInternalException)ex.getCause()).getInternalCause();
            }
            failure = ex;
        } catch (Exception ex) {
            failure = ex;

        }
        fail(new MException(String.format(
                "invocation error: %s.%s",
                instance.getClass().getName(),
                mName),
                failure));
        return null;
    }

    // TODO, introduire une annotation genre @MField.... sur la property
    @Primitive
    public static Value getField(
            final CodeRunner ctxt, Value objRef, Value fieldName) {
        logger.info("== getField ==");
        return Value.UNIT;
    }

    // TODO, introduire une annotation genre @MField.... sur la property
    @Primitive
    public static Value setField(
            final CodeRunner ctxt, Value objRef, Value args) {
        logger.info("== setField ==");
        return Value.UNIT;
    }
}